Object Oriented Programming - Decorators and Multiple Inheritance

Decorators

In mathematics higher order functions are ones which take function/s as arguments and return a function as a result. Such capabilites are implemented in Python by using decorators.

Of course, you can implement a similar functionality using combination of def and lambda. However, it is generally unsafe practice to use lambda methods. In fact, the creator of Python language, Guido Van Rossum suggested its removal but the whole community of programmers was too used to it, protested against its removal and hence it remained.

A decorator is a function which takes as input another function and extends its behaviour/capability without making any explicit changes to it.

First of all you need to understand the idea of first class objects.

A first class object is an language entity that can be treated as a native variable. That means it can be created, destroyed, passed as an argument to a function, printed as you wish, etc.


In [ ]:
def func2(func1):
    return func1 + 1

def func3(func2, arg):# Here func2 is being passed as a parameter.
    return func2(arg)

print(func2(2))
print(func3(func2, 3))

In [ ]:
def user_defined_decorator(function1):
    def wrapper():
        print("This statement is being printed before the passed function is called.")
        function1()
        print("This statement is being printed after the passed function is called.")

    return wrapper

@us
def task():
    print("Lite")

user_defined_decorator(task)()

Common decorators

@staticmethod acts as a wrapper and informs the interpreter that the method is one which does not depend on the class or the object. It is just a method which is logical to include in the class body.

@classmethod acts as a wrapper and informs the interpreter that the method is one which depends on the class. This can be clearly understood cause the first argument is interpreted as the class type. It is a method that is commonly shared by all objects of the class type.


In [ ]:
class BITSian():
    def __init__(self, name, bitsian=True):
        self.name = name
        self.bitsian = bitsian

    @staticmethod
    def is_object():
        return True

    def is_human(cls):
        print(cls)
        return True

    def get_name(self):
        print(self)
        return self.name

    def is_bitsian(self):
        return str(self.name + " is a BITSian : " + str(self.bitsian))

p = BITSian("Reuben D'Souza")
print(p.get_name())
print(p.is_bitsian())

# If @staticmethod wasn't there, then this would result in an error cause arguments don't match. 
print(p.is_object())

# If @classmethod wasn't there, then this "p" would be interpreted as "object" type and not "class" type.
print(classmethod(p.is_human)())

In [ ]:
class BITSian():
    def __init__(self, name, bitsian=True):
        self.k = name
        self.bitsian = bitsian

    @staticmethod
    def is_object():
        return True

    @classmethod
    def is_human(cls):
        return True

    @property
    def name(self):
        print("TEST getter")
        return self.k
    
    @name.setter
    def name(self, name):
        print("TEST setter")
        self.k = name
    
    def is_bitsian(self):
        return str(self.name + " is a BITSian : " + str(self.bitsian))
    
    def __str__(self):
        return "BITSian : " + self.name
    
p = BITSian("Keerthana")
print(p)
p.name ="Rohan Prabhu"
print(p)

@slow, @XFAIL, etc are decorators used in unit testing(i.e. pytest). They will make sense only when unit testing is taught.

Inheriting from Multiple Classes


In [ ]:
class A():
    def save(self):
        print("Save in A")

class B():
    def save(self):
        print("Save in B")

class C(A,B):
    def __init__(self):
        pass
    
a = A()
c = C()
b = B()

The Diamond Problem

Consider a situation where there is one parentclass A and then two more subclasses B and C. Then consider a further subclass D inheriting from B and C both. If there be a method defined in A which is inherited in B and C and then overidden, which one will D use ?


In [ ]:
class A:
    def test(self):
        print("Test of A called")

class B(A):
    def test(self):
        print("Test of B called")
    
class C(A):
    def test(self):
        print("Test of C called")

class D(C, B):
    pass

print(D.mro())
#d = D()
#d.test()
D.mro()[2].test(d)# This is a terrible thing to write in development level code!! Think about re-implementation.

If you write properly structured code then you should never run into the diamond paradox. If there are workarounds allowing you to override the Method Resolution Order and access the superclass B method then don't do it. Not advisable at all.

Instead think about how to restructure your code.